"""

.. module:: config_service
.. moduleauthor:: @author: 

*Description*:

This module contains the ConfigService class.

*Constants*:

.. seealso:: class :class:`runtime.service.Service`
"""

# Imports
#
import datetime
import threading
import time
import logging

from datastore.resource import *
from datastore.uri import *
from runtime.service import *
from runtime.node import LoggingLevels
from datastore.spaces import Spaces

# Constants
#

# Classes
#

class ConfigService(Service):
    """
    This service is hard-coded to start in runtime.Runtime.run().
    
    **Constants**:
    """

    DC_TITLE = 'dc:title'
    DC_DESCRIPTION = 'dc:description'
    AUTOLOAD_PREFIX = 'autoload_prefix'
    TEMPLATE = 'template'
    REFERENCE = 'reference'
    TERMTYPE = 'termtype'
    VERSION = 'version'
    NOTE = 'note'
    METADATA = 'metadata'
    CONTENT = 'content'
    CLASSNAME = 'classname'
    OPTIONS = 'options'
    API = 'api'
    SERVICES = 'services'
    BINDINGS = 'bindings'
    DEFINITIONS = 'definitions'
    BIND = 'bind'
    
    def cb_start(self, uri, action, res):
        """
        :param uri: Uri
        :param action:
        :param res: Resource
        """
        
        # configuration instances are stored here, e.g.
        # internal:base/config/base/datastore
        self.instances = Uri('internal:base/config/')

        # watch (cb_config_update) for new configuration resources.
        self.spaces.set_watch(self.instances,
                               self.cb_config_update, filter=['put', 'update'])

        # load and process the configuration files (in sequence order).
        try:
            config_list = self.datastore.find_type(
                Uri('term:base/runtime/configuration'))
            sorted_config_list = self.sort_config_list_by_sequence_number(config_list)
            self.load_configs(sorted_config_list)
        except:
            self.logger.log(logging.WARNING, "No Configuration Files.")
                        
    def cb_config_update(self, uri, action, res):
        """
        :param uri: Uri
        :param action:
        :param res: Resource
        """
        
        # for now when we update a config, we'll just save the config in the 
        # datastore and advise the user to restart the node to see the changes.
        # This is because we can't reliability restart a service right now while a node 
        # is running. For example what happens if another service is relying on the service
        # that is restarting. Who services the requests while the service is restarting? 
        # Do we fix these with queues. So instead of processing a uri you process a uri that
        # represents a queue? 
        if action == Spaces.ACTION_UPDATE:
            self.datastore.put(res)
            return
        
        self.logger.log(logging.INFO, "Processing configuration (%s)",
            res.dc_title())

        # process bindings
        self.process_bindings(res)

        # process definitions (definitions_list is used later by autoload)
        definition_ress_dict = self.process_definitions(res)

        # process services (just create the service definition resources)
        self.process_services(res, action)

        # process resources to autoload from the datastore into spaces.
        # suffix metadata is automatically added to files by build_datastore.py
        self.autoload_resources_defined_in_config(definition_ress_dict)

    def create_definition_rs(self, definition):
        definition_res = Resource(type='term:base/runtime/definition')
        if ConfigService.DC_TITLE in definition.keys():
            definition_res.set_metadata(Uri(ConfigService.DC_TITLE), 
                definition[ConfigService.DC_TITLE].strip())
        if ConfigService.DC_DESCRIPTION in definition.keys():
            definition_res.set_metadata(Uri(ConfigService.DC_DESCRIPTION), 
                definition[ConfigService.DC_DESCRIPTION].strip())
        if ConfigService.AUTOLOAD_PREFIX in definition.keys():
            definition_res.autoload_prefix = \
                definition[ConfigService.AUTOLOAD_PREFIX]
        # process template
        if ConfigService.TEMPLATE in definition.keys():
            definition_res.template = definition[ConfigService.TEMPLATE]
        # dublin core elements: reference, termtype, version, note
        if ConfigService.REFERENCE in definition.keys():
            definition_res.reference = definition[ConfigService.REFERENCE]
        if ConfigService.TERMTYPE in definition.keys():
            definition_res.termtype = definition[ConfigService.TERMTYPE]
        if ConfigService.VERSION in definition.keys():
            definition_res.version = definition[ConfigService.VERSION]
        if ConfigService.NOTE in definition.keys():
            definition_res.note = definition[ConfigService.NOTE].strip()
        if ConfigService.METADATA in definition.keys():
            for metadata in definition[ConfigService.METADATA].keys():
                self.logger.log(LoggingLevels.TRACE, 
                    "    metadata %s", metadata)
            definition_res.metadata = definition[ConfigService.METADATA]
        if ConfigService.CONTENT in definition.keys():
            for content in definition[ConfigService.CONTENT].keys():
                self.logger.log(LoggingLevels.TRACE, 
                    "    content %s", content)
            definition_res.content = definition[ConfigService.CONTENT]
        return definition_res
    
    def create_service_definition_rs(self, service):
        service_def_res = Resource(type='term:base/runtime/service')
        if ConfigService.DC_TITLE in service.keys():
            service_def_res.set_metadata(Uri(ConfigService.DC_TITLE), 
                service[ConfigService.DC_TITLE].strip())
        if ConfigService.DC_DESCRIPTION in service.keys():
            service_def_res.set_metadata(Uri(ConfigService.DC_DESCRIPTION), 
                service[ConfigService.DC_DESCRIPTION].strip())
        if ConfigService.CLASSNAME in service.keys():
            service_def_res.classname = service[ConfigService.CLASSNAME].strip()
        if ConfigService.OPTIONS in service.keys():
            service_def_res.options = service[ConfigService.OPTIONS]
        if ConfigService.API in service.keys():
            service_def_res.api = service[ConfigService.API]
        return service_def_res
    
    def process_bindings(self, config_res):
        if config_res.has_var(ConfigService.BINDINGS):
            bindings_list = config_res.bindings
        else:
            return
        for key in bindings_list.keys():
            self.logger.log(logging.DEBUG, "  binding %s=%s", 
                key, bindings_list[key][ConfigService.BIND])
            Uri.add_namespace(key, bindings_list[key][ConfigService.BIND])
            
    def process_definitions(self, config_res):
        definition_ress = {}
        if config_res.has_var(ConfigService.DEFINITIONS):
            definitions_list = config_res.definitions
        else:
            return definition_ress
        for key in definitions_list.keys():
            self.logger.log(logging.DEBUG, "  definition %s", key)
            definition = definitions_list[key]
            definition_res = self.create_definition_rs(definition)
            self.spaces.put(Uri(key), definition_res, warning=False)
            definition_ress[Uri(key)] = definition_res
        return definition_ress        
            
    def process_services(self, config_res, action):
        service_instances = Uri('internal:base/services-list/')
        if config_res.has_var(ConfigService.SERVICES) and config_res.get_var(ConfigService.SERVICES):
            services_list = config_res.services
        else:
            return
        if len(services_list.keys()) > 1:
            error_msg = "More than one service definition in configuration file %s" % config_res.dc_title()
            self.logger.log(logging.ERROR, error_msg)
            raise Exception(error_msg)
        key, service = services_list.items()[0]
        uri = service_instances + key
        # create a new resource that contains the service definition  
        # for a service and put it to the service's uri
        service_def_res = self.create_service_definition_rs(service)
        self.spaces.put(uri, service_def_res)
                
    def autoload_resources_defined_in_config(self, definition_ress_dict):
        if not definition_ress_dict:
            return
        suffix_uri = Uri('term:base/build/autoload_suffix')
        for res_type_def_uri, res_type_def_rs in definition_ress_dict.items():
            if res_type_def_rs.has_var(ConfigService.AUTOLOAD_PREFIX):
                try:
                    ress_that_are_to_be_autoloaded = self.datastore.find_type(res_type_def_uri)
                except:
                    continue
                for res in ress_that_are_to_be_autoloaded:
                    if res.metadata_has_key(suffix_uri):
                        uri = res_type_def_rs.autoload_prefix + \
                            res.get_metadata(suffix_uri)
                        if self.spaces.exists(uri):
                            # remove the old resource in spaces
                            self.spaces.remove(uri)
                        self.spaces.put(uri, res)
                        self.logger.log(LoggingLevels.TRACE, 
                                    "   autoload loaded %s", uri)
                        
    def sort_config_list_by_sequence_number(self, config_list):
        sorted_config = {}
        sorted_config_list = []
        for config in config_list:
            if config.sequence in sorted_config.keys():
                error_msg = "Duplicate configuration sequence (%s) for config %s and %s" % (config.sequence, config.dc_title(), sorted_config[config.sequence].dc_title())
                self.logger.log(logging.ERROR, error_msg)
                raise Exception(error_msg)
            else:
                # first configuration with that sequence number
                sorted_config[config.sequence] = config
        for config_key in sorted(sorted_config.keys()):
            sorted_config_list.append(sorted_config[config_key])
        return sorted_config_list

    def load_configs(self, sorted_config_list):
        active_uri = Uri('term:base/runtime/active')
        for config_res in sorted_config_list:
            # If active not specified then assume configuration is active.
            if config_res.metadata_has_key(active_uri):
                if not config_res.get_metadata(active_uri):
                    self.logger.log(logging.DEBUG, "Inactive configuration (%s) not loaded.", config_res.dc_title(default=''))
                    continue
            self.logger.log(logging.DEBUG, "processing (%s) %s",
                        config_res.sequence, config_res.dc_title(default=''))
            if config_res.metadata_has_key(Uri('term:base/build/autoload_suffix')):
                config_uri = self.instances + config_res.get_metadata(
                            Uri('term:base/build/autoload_suffix'))
                self.spaces.put(config_uri, config_res, warning=False)
            else:
                error_msg = "Could not load configuration %s. Missing autoload_suffix." % config_res.dc_title()
                self.logger.log(logging.ERROR, error_msg)
                raise Exception(error_msg)